package gu.sql2java.excel.aspect.spring;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.reflect.TypeToken;

import gu.sql2java.BaseBean;
import gu.sql2java.SimpleLog;
import gu.sql2java.excel.ExcelGenerator;
import gu.sql2java.excel.annotations.ExcelSheet;
import gu.sql2java.excel.config.SheetConfig;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicReference;

import javax.servlet.http.HttpServletResponse;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.nullToEmpty;
import static net.gdface.utils.BeanPropertyUtils.isEmpty;
import static gu.sql2java.excel.utils.MethodSupport.*;

@Component
public class ExcelHelperAround{
    private static final String SUFFIX_ZIP = ".zip";
    private static final String SUFFIX_XLSX = ".xlsx";
	private static final ThreadLocal<SheetConfig> sheetConfig = new ThreadLocal<>();
	private static final ThreadLocal<Boolean> forceFlag = new ThreadLocal<>();
	/** 不需要注入的参数名  */
	private static final ImmutableSet<String> unmodifiedFields=ImmutableSet.of("hideColumns","defaultIncludeColumns");

	private static ReturnValueParserDefaultImpl DEFAULT_PARSER = new ReturnValueParserDefaultImpl();

	/** 所有在IOC注册中的{@link ReturnValueParser}接口实例  */
	@Autowired
	private Map<String, ReturnValueParser> returnValueParses = new HashMap<>(); 	
	/**
	 * 返回值类型对应的{@link ReturnValueParser}实例
	 */
	private final LoadingCache<Class<?>, ReturnValueParser> parserCache = 
			CacheBuilder.newBuilder().build(
				new CacheLoader<Class<?>, ReturnValueParser>(){
					@Override
					public ReturnValueParser load(Class<?> key) throws Exception {
						return Iterables.tryFind(returnValueParses.values(), r->r.getReturnType().isAssignableFrom(key)).or(DEFAULT_PARSER);
				}}); 

	/**
	 * ExcelHelper切面执行<br>
	 * 对于有{@link ExcelSheet}注解的服务方法,自动根据注解创建{@link SheetConfig}实例,
	 * 并将服务方法中与{@link ExcelSheet}注解定义的方法名同名的参数注入到上面的{@link SheetConfig}实例中.
	 * 将服务方法返回的数据记录列表执行{@link #springWebExport(SheetConfig, Integer, String, ExcelGenerator)}方法输出excel格式数据<br>
	 * 因为需要获取服务方法的参数名，所以需要服务方法所在项目Java编译器如下设置指定{@code -parameters}参数
	 * <pre>
	 * &lt;build&gt;
	 *     &lt;pluginManagement&gt;
	 *         &lt;plugins&gt;
     *             &lt;plugin&gt;
     *                 &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
     *                 &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
     *                 &lt;configuration&gt;
     *                     &lt;compilerArgs&gt;
     *                         &lt;compilerArg&gt;-Xlint:unchecked&lt;/compilerArg&gt;
     *                         &lt;compilerArg&gt;-parameters&lt;/compilerArg&gt;
     *                     &lt;/compilerArgs&gt;
     *                 &lt;/configuration&gt;
     *             &lt;/plugin&gt;
     *         &lt;/plugins&gt;
     *     &lt;/pluginManagement&gt;
	 * &lt;/build&gt;
	 * </pre>
	 * @param joinPoint
	 * @return 返回执行服务方法调用的返回值
	 * @throws Throwable
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public Object excelAround(ProceedingJoinPoint joinPoint) throws Throwable {
		/** 在调用服务方法前清除TLS变量 */
		sheetConfig.remove();
		forceFlag.remove();
		MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
		Method method = methodSignature.getMethod();
	    ExcelSheet excelSheet = method.getAnnotation(ExcelSheet.class);
	    if(null == excelSheet){
	    	/** 未激活ExcelHelper 直接返回原方法 */
	        return  joinPoint.proceed();
	    }
	    SheetConfig sheetConfig = getSheelConfig(method);
    	AtomicReference<Integer> getParameter = new AtomicReference();
    	AtomicReference<String> exportFileName = new AtomicReference<>();
    	injectParameter(joinPoint, excelSheet, sheetConfig, method, getParameter, exportFileName);
    	ReturnValueParser returnValueParser = parserCache.get(method.getReturnType());
    	if(null != getParameter.get()){
    		checkState(!Object.class.equals(sheetConfig.getBeanClass()),"NOT DEFINED sheetConfig.beanClass");
    		ExcelGenerator generator = 
        			new ExcelGenerator(sheetConfig.getBeanClass(),  
        					sheetConfig.getIncludeColumnsOrDefault());
    		Object webValue= springWebExport(sheetConfig, getParameter.get(), exportFileName.get(),generator);
			/**
			 * 返回 getParameter 数据
			 */
			return returnValueParser.onGetParameter(null, webValue);
    	}
    	Object returnValue = joinPoint.proceed();
	    if(!returnValueParser.isSuccess(returnValue)){
	    	return returnValue;
	    }
	    final ReturnInfo targetReturnInfo = returnValueParser.parse(new ReturnInfo(returnValue, method));
	    final Class<?> targetRawReturnType = TypeToken.of(targetReturnInfo.returnType).getRawType();
		/**
		 * 根据方法返回类型提取集合内的元素类型
		 */
	    final Class<?> elementType;
	    if(BlockingQueue.class.isAssignableFrom(targetRawReturnType)){
	        Type subType = ((ParameterizedType)targetReturnInfo.returnType).getActualTypeArguments()[0];
	        Class subRawType = TypeToken.of(subType).getRawType();
	        if(subRawType.isArray() || Iterable.class.isAssignableFrom(subRawType)){
	            elementType = extractElementType(targetRawReturnType,subType);	            
	        }else {
	            elementType = extractElementType(targetRawReturnType,targetReturnInfo.returnType);
            }
	    }else {
            elementType = extractElementType(targetRawReturnType,targetReturnInfo.returnType);
        }
		if(Map.class.isAssignableFrom(elementType)){
			
		}else if(BaseBean.class.isAssignableFrom(elementType)){
			
		}else if(SheetConfig.mayBeJavaBean(elementType)){
			
		}else {
			throw new IllegalArgumentException(String.format("UNSUPPORTED element type %s",elementType.getName()));
		}
		
	    try {
        	ExcelGenerator generator = 
        			new ExcelGenerator(targetReturnInfo.returnValue, elementType,  sheetConfig.getIncludeColumnsOrDefault());
    		springWebExport(sheetConfig, getParameter.get(), exportFileName.get(),generator);
    		return null;
		} catch (Exception e) {
			SimpleLog.log(e);
			returnValueParser.onError(returnValue, e);
			return returnValue;
		}
	}
	@SuppressWarnings({ "rawtypes", "unchecked" })
    private Class extractElementType(Class<?> targetRawReturnType,Type returnType){
	    if(targetRawReturnType.isArray()){
            return targetRawReturnType.getComponentType();
        }else if(Iterable.class.isAssignableFrom(targetRawReturnType)){
            /**
             * 使用TypeToken.getSuperType方法比直接通过 ParameterizedType.getActualTypeArguments
             * 获取元素类型有更好的适应性,可以支持从类似 JSONArray这样的非ParameterizedType类型提取元素类型
             * */
            TypeToken typeToken = TypeToken.of(returnType);
            TypeToken superType = typeToken.getSupertype(Iterable.class);
            return TypeToken.of(((ParameterizedType)superType.getType()).getActualTypeArguments()[0])
                    .getRawType();
        }else {
            throw new IllegalArgumentException(String.format("UNSUPPORTED RETURN TYPE %s",targetRawReturnType.getName()));
        }
	}
	private SheetConfig getSheelConfig(Method method){
    	SheetConfig sheetConfig;
    	if(null != ExcelHelperAround.sheetConfig.get()){
    		if(Boolean.TRUE.equals(ExcelHelperAround.forceFlag.get())){
    			sheetConfig = ExcelHelperAround.sheetConfig.get();
    		}else {
    			/** 
    			 * 如果调用 setSheetConfig,或setBeanClass方法定义了 SheetConfig,
    			 * 则将基于从服务方法注解生成的SheetConfig合并此对象
    			 */
    			sheetConfig = new SheetConfig(method).merge(ExcelHelperAround.sheetConfig.get());
			}
    	}else {
    		sheetConfig = new SheetConfig(method);/** 服务方法注解生成的Excel配置对象 */
		}
    	ExcelHelperAround.sheetConfig.set(sheetConfig);
    	return sheetConfig;
	}
	/**
	 * 为Spring AOP处理当前Excel导出指定的Excel配置对象
	 * @param sheetConfig
	 * @param force 为{@code true}强制替换ExcelGenerator生成的{@link SheetConfig}对象
	 */
	public static void setSheetconfig(SheetConfig sheetConfig, boolean force) {
		ExcelHelperAround.sheetConfig.set(sheetConfig);
		ExcelHelperAround.forceFlag.set(force);		
	}
	/**
	 * (非强制)为Spring AOP处理当前Excel导出指定的Excel配置对象
	 * @see #setSheetconfig(SheetConfig, boolean)
	 */
	public static void setSheetconfig(SheetConfig sheetConfig) {
		setSheetconfig(sheetConfig,false);
	}
	/**
	 * (非强制)为Spring AOP处理当前Excel导出指定的阻塞队列超时时间(秒)
	 * @param queueTimeout
	 */
	public static void setQueueTimeout(int queueTimeout){
	    getSheetconfig().setQueueTimeout(queueTimeout);
	}
	/**
	 * (非强制)为Spring AOP处理当前Excel导出指定的阻塞队列记录总数
	 * @param totalRowCount
	 */
	public static void setTotalRowCount(long totalRowCount){
	    getSheetconfig().setTotalRowCount(totalRowCount);
	}
	/**
	 * 返回为Spring AOP处理当前Excel导出的Excel配置对象
	 * @since 3.25.4
	 */
	public static SheetConfig getSheetconfig(){
		if(null == sheetConfig.get()){
			sheetConfig.set(new SheetConfig());
		}
		return sheetConfig.get();
	}
	/**
	 * 导出{@link ExcelGenerator}实例的数据记录生成Excel格式的数据输出到HTTP Response<br>
	 * {@code getParameter}为{@code null}时,输出Excel数据, Response Content Type需要设置为{@code application/octet-stream}
	 * {@code getParameter}不为{@code null}时,根据{@code getParameter}的值返回JSON格式的结果,Response Content Type需要设置为{@code application/json}
	 * @param sheetConfig 
	 * @param getParameter Response Content Type需要设置为{@code application/json}
	 * <ul>
	 * <li>1 返回所有可选的字段列表,参见{@link SheetConfig#getAvailableColumns()}</li>
	 * <li>2 返回默认输出的字段名及字段显示名称,参见{@link SheetConfig#getDefaultExportColumns()}</li>
	 * <li>3 返回默认输出的字段名,参见{@link SheetConfig#getDefaultExportColumnNames()}</li>
	 * </ul>
     * @param exportFileName 指定导出的excel文件名和格式后缀,不指定文件名则自动以日期命名,
     *                                         支持的输出格式后缀为.csv,xlsx,如果要输出zip压缩格式,在输出格式后缀增加.zip，如.csv.zip即输出,压缩的.csv文件，
     *                                         如果没有指定输出格式后缀默认输出不压缩的.xlsx.
	 * @return {@code getParameter}不为{@code null}时返回数据,否则返回 {@code null}
	 * @throws IOException
	 */
	private static Object springWebExport(
			SheetConfig sheetConfig, Integer getParameter,String exportFileName,ExcelGenerator generator) throws IOException {
		if(Boolean.TRUE.equals(ExcelHelperAround.forceFlag.get())){
			/** forceFlag 为true时强制使用 TLS变量存储的对象 */
			generator.setSheetConfig(ExcelHelperAround.sheetConfig.get());
		}else {
			generator.getSheetConfig().merge(sheetConfig);
			if(null != ExcelHelperAround.sheetConfig.get()){
			    generator.getSheetConfig().merge(ExcelHelperAround.sheetConfig.get());
			}
		}
	
		RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
		checkState(requestAttributes instanceof ServletRequestAttributes,"ServletRequestAttributes is required");
		HttpServletResponse  response= ((ServletRequestAttributes)requestAttributes).getResponse();
		if(null != getParameter){
			response.setContentType(MediaType.APPLICATION_JSON_VALUE);
			Object returnValue;
			switch(getParameter){
			case 1:
				/** 返回所有可输出字段及字段显示名称 */
				returnValue = generator.getSheetConfig().getAvailableColumns();
				break;
			case 2:
				/** 返回默认输出的字段名及字段显示名称 */
				returnValue = generator.getSheetConfig().getDefaultExportColumns();
				break;
				/** 返回默认输出的字段名 */
			case 3:
				returnValue = generator.getSheetConfig().getDefaultExportColumnNames();
				break;
			default:
				throw new IllegalArgumentException(String.format("INVALID getParameter %d",getParameter));
			}
			return returnValue;
		}
		/** 执行EXCEL导出流程 */
		response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
		String formatSuffix = SUFFIX_XLSX;
		boolean zip = false;
		/** 设置导出文件名 */
		exportFileName = nullToEmpty(exportFileName).trim();
		if(exportFileName.isEmpty()){
			exportFileName = makeDefaultExportFileName(sheetConfig);
		}else {
		    if(exportFileName.endsWith(SUFFIX_ZIP)){
		        zip = true;
		        exportFileName = exportFileName.substring(0,exportFileName.length() - SUFFIX_ZIP.length());
		        if(exportFileName.isEmpty()){
		            exportFileName = makeDefaultExportFileName(sheetConfig);
		        }
		    }
		    int lastDot = exportFileName.lastIndexOf('.');
		    if(lastDot > 0){
		        formatSuffix = exportFileName.substring(lastDot);
		        exportFileName = exportFileName.substring(0, lastDot);
		    }else if(0 == lastDot){
		        formatSuffix = exportFileName;
		        exportFileName = makeDefaultExportFileName(sheetConfig);
		    }
        }
		response.setHeader("Content-Disposition", "attachment; filename=" + exportFileName + formatSuffix + (zip ? SUFFIX_ZIP: ""));
		generator.generate(response,formatSuffix, zip ? exportFileName + formatSuffix : null);
		return null;
	}
	private static String makeDefaultExportFileName(SheetConfig sheetConfig){
	    String prefix = firstNonNull(sheetConfig.getFileNamePrefix(),sheetConfig.getSheetName());
        SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd_HHmmss");
        String currentDateTime = dateFormatter.format(new Date());
        return  prefix + currentDateTime ;
	}
	/**
	 * 将Spring Controller服务方法中与{@link ExcelSheet}注解定义的方法名同名的参数注入到{@link SheetConfig}实例中.
	 * @param joinPoint
	 * @param excelSheet
	 * @param sheetConfig
	 * @param method
	 * @param getParameter [out]服务方法中的getParameter参数
	 * @param exportFileName [out]服务方法中的exportFileName参数
	 * @throws Exception
	 */
	private static void injectParameter(ProceedingJoinPoint joinPoint ,ExcelSheet excelSheet,SheetConfig sheetConfig,Method method,AtomicReference<Integer> getParameter,AtomicReference<String> exportFileName) throws Exception{
		List<String> methodNames = methodNamesOf(excelSheet);
		Parameter[] parameters = method.getParameters();
		Object[] args = joinPoint.getArgs();
		/** 将服务接口方法中的与 ExcelSheet 注解中字段名匹配的所有参数值注入sheetConfig */
		for(int i = 0;i<parameters.length;++i){
			String name = parameters[i].getName();
			Object value = args[i];
			if(methodNames.contains(name) && !unmodifiedFields.contains(name)){
				if(!isEmpty(value)){
					invokeWriteMethod(sheetConfig,name,value);
					SimpleLog.log("INJECT field {} value  {}",name,value);
				}
			}else if(name.equals(excelSheet.getParameterArgName()) && value instanceof Number){
				getParameter.set(((Number)value).intValue());
			}else if(name.equals(excelSheet.exportFileNameArgName()) && value instanceof String){
				exportFileName.set((String)value);
			}
		} 
	}    
}
